/*
 * Copyright 2000-2025 JetBrains s.r.o.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.jetbrains.internal.jbrapi;

import com.jetbrains.exported.JBRApi.Provides;

import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Supplier;

import static java.lang.invoke.MethodHandles.Lookup;

/**
 * JBR API is a collection of JBR-specific features that are accessed by client though
 * {@link com.jetbrains.JBR jetbrains.runtime.api} module. Actual implementation is linked by
 * JBR at runtime by generating {@linkplain Proxy proxy objects}.
 * Mapping between interfaces and implementation code is defined using
 * {@link com.jetbrains.exported.JBRApi.Provided} and {@link com.jetbrains.exported.JBRApi.Provides} annotations.
 * <p>
 * This class is an entry point into JBR API backend.
 * @see Proxy
 */
public class JBRApi {
    /**
     * Enable JBR API, it wouldn't init when disabled. Enabled by default.
     */
    public static final boolean ENABLED = Utils.property("jetbrains.runtime.api.enabled", true);
    /**
     * Enable API extensions. When disabled, extension methods are treated like any other method,
     * {@link JBRApi#isExtensionSupported} always returns false, {@link JBRApi#getService(Class, Enum[])}
     * behaves the same as {@link JBRApi#getService(Class)}. Enabled by default.
     */
    static final boolean EXTENSIONS_ENABLED = Utils.property("jetbrains.runtime.api.extensions.enabled", true);
    /**
     * Enable extensive debugging logging. Disabled by default.
     */
    public static final boolean VERBOSE = Utils.property("jetbrains.runtime.api.verbose", false);
    /**
     * Print warnings about usage of deprecated interfaces and methods to {@link System#err}. Enabled by default.
     */
    static final boolean LOG_DEPRECATED = Utils.property("jetbrains.runtime.api.logDeprecated", true);
    /**
     * Enable additional verification of generated bytecode. Disabled by default.
     */
    static final boolean VERIFY_BYTECODE = Utils.property("jetbrains.runtime.api.verifyBytecode", false);
    /**
     * Allow extending registry. Disabled by default, used for tests.
     */
    private static final boolean EXTEND_REGISTRY = Utils.property("jetbrains.runtime.api.extendRegistry", false);

    record DynamicCallTargetKey(Class<?> proxy, String name, String descriptor) {}
    static final ConcurrentMap<DynamicCallTargetKey, Supplier<MethodHandle>> dynamicCallTargets = new ConcurrentHashMap<>();
    private static final ProxyRepository proxyRepository = new ProxyRepository();

    private static Boolean[] supportedExtensions;
    private static long[] emptyExtensionsBitfield;
    @SuppressWarnings("rawtypes")
    private static Map<Enum<?>, Class[]> knownExtensions;
    static Function<Method, Enum<?>> extensionExtractor;

    @SuppressWarnings("rawtypes")
    public static void init(InputStream extendedRegistryStream,
                            Class<? extends Annotation> serviceAnnotation,
                            Class<? extends Annotation> providedAnnotation,
                            Class<? extends Annotation> providesAnnotation,
                            Map<Enum<?>, Class[]> knownExtensions,
                            Function<Method, Enum<?>> extensionExtractor) {
        if (extendedRegistryStream != null && !EXTEND_REGISTRY) {
            throw new Error("Extending JBR API registry is not supported");
        }
        proxyRepository.init(extendedRegistryStream, serviceAnnotation, providedAnnotation, providesAnnotation);

        if (EXTENSIONS_ENABLED) {
            JBRApi.knownExtensions = knownExtensions;
            JBRApi.extensionExtractor = extensionExtractor;
            supportedExtensions = new Boolean[
                    knownExtensions.keySet().stream().mapToInt(Enum::ordinal).max().orElse(-1) + 1];
            emptyExtensionsBitfield = new long[(supportedExtensions.length + 63) / 64];
        }

        if (VERBOSE) {
            System.out.println("JBR API init\n  knownExtensions = " + (EXTENSIONS_ENABLED ? knownExtensions.keySet() : "DISABLED"));
        }
    }

    public static MethodHandle bindDynamic(Lookup caller, String name, MethodType type) {
        if (VERBOSE) {
            System.out.println("Binding call site " + caller.lookupClass().getName() + "#" + name + ": " + type);
        }
        if (!caller.hasFullPrivilegeAccess()) throw new Error("Caller lookup must have full privilege access"); // Authenticity check.
        return dynamicCallTargets.get(new DynamicCallTargetKey(caller.lookupClass(), name, type.descriptorString())).get().asType(type);
    }

    /**
     * @return JBR API version supported by current implementation.
     */
    @Provides("JBR.ServiceApi")
    public static String getImplVersion() {
        return proxyRepository.getVersion();
    }

    /**
     * @param extension extension name
     * @return true if all methods belonging to given extension are supported
     * @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
     * service, but is not directly exposed to user.
     */
    @Provides("JBR.ServiceApi")
    public static boolean isExtensionSupported(Enum<?> extension) {
        if (!EXTENSIONS_ENABLED) return false;
        int i = extension.ordinal();
        if (supportedExtensions[i] == null) {
            synchronized (JBRApi.class) {
                if (supportedExtensions[i] == null) {
                    boolean result = true;
                    for (Class<?> c : knownExtensions.get(extension)) {
                        result &= proxyRepository.getProxy(c, null).isExtensionSupported(extension);
                    }
                    supportedExtensions[i] = result;
                }
            }
        }
        return supportedExtensions[i];
    }

    /**
     * @return fully supported service implementation for the given interface with specified extensions, or null
     * @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
     * service, but is not directly exposed to user.
     */
    @Provides("JBR.ServiceApi")
    public static <T> T getService(Class<T> interFace, Enum<?>... extensions) {
        if (!EXTENSIONS_ENABLED) return getService(interFace);

        long[] bitfield = new long[emptyExtensionsBitfield.length];
        for (Enum<?> e : extensions) {
            if (isExtensionSupported(e)) {
                int i = e.ordinal() / 64;
                int j = e.ordinal() % 64;
                bitfield[i] |= 1L << j;
            } else {
                if (VERBOSE) {
                    Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Extension not supported: " + e.name());
                }
                return null;
            }
        }

        return getService(interFace, bitfield, true);
    }

    /**
     * @return fully supported service implementation for the given interface, or null
     * @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
     * service, but is not directly exposed to user.
     */
    @Provides("JBR.ServiceApi")
    public static <T> T getService(Class<T> interFace) {
        return getService(interFace, emptyExtensionsBitfield, true);
    }

    public static <T> T getInternalService(Class<T> interFace) {
        return getService(interFace, emptyExtensionsBitfield, false);
    }

    @SuppressWarnings("unchecked")
    private static <T> T getService(Class<T> interFace, long[] extensions, boolean publicService) {
        Proxy p = proxyRepository.getProxy(interFace, null);
        if ((p.getFlags() & Proxy.SERVICE) == 0 || (publicService && (p.getFlags() & Proxy.INTERNAL) != 0)) {
            if (VERBOSE) {
                Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Not allowed as a service: " + interFace.getCanonicalName());
            }
            return null;
        }
        if (!p.init()) {
            if (VERBOSE) {
                Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Service not supported: " + interFace.getCanonicalName());
            }
            return null;
        }
        try {
            MethodHandle constructor = p.getConstructor();
            return (T) (EXTENSIONS_ENABLED ? constructor.invoke(extensions) : constructor.invoke());
        } catch (com.jetbrains.exported.JBRApi.ServiceNotAvailableException | NullPointerException e) {
            if (VERBOSE) {
                synchronized (System.err) {
                    Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Service not available: " + interFace.getCanonicalName());
                    System.err.print("Caused by: ");
                    e.printStackTrace(System.err);
                }
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        return null;
    }
}
